import "babel-polyfill";
import {MathError, ZeroDivisionError} from "../error";
import Integer from "../number/integer";
import {gcd} from "../number-theory/gcd";
import PolynomialTemplate from "./polynomial";

const {zero, one, eq, abs, negate, add, subtract, multiply, square, divmod, exactDivide, shl} = Integer;

var IntType = n => Integer.from(n);
Object.assign(IntType, {
    zero,
    one,
    isZero: n => n.sign === 0,
    isOne: n => eq(n, one),
    eq,
    negate,
    add,
    subtract,
    multiply,
    square
});

var trim = function() {
    while (this.length && this[this.length - 1].sign === 0) {
        this.pop();
    }
    return this;
};

export var CannotDivideError = class extends MathError {
    name = "CannotDivideError";

    constructor(message) {
        super(message);
        this.message = message;
    }

    get [Symbol.toStringTag]() {
        return "CannotDivideError";
    }
};

export default class PolynomialZ extends PolynomialTemplate(IntType) {
    _fromString(string, symbol = "x") {
        if (!string) {
            throw new MathError("Invalid String");
        } else if (!/^[_A-Za-z][_A-Za-z0-9]*$/.test(symbol)) {
            throw new MathError(`Invalid Symbol ${symbol}`);
        }

        var throw_error = function() {
            throw new MathError(`Invalid String ${string}`);
        };
        var trim = function() {
            while (string[++offset] === " ");
        };
        var op_priority = {
            "+": 0,
            "-": 0,
            "*": 1,
            "^": 2
        };
        var do_operation = function(s) {
            if (stack.length === 1) {
                if (arguments.length) {
                    stack.unshift(s);
                }
            } else {
                if (arguments.length === 0 || op_priority[s] <= op_priority[stack[stack.length - 3]]) {
                    let p2 = stack.pop();
                    let p1 = stack.pop();
                    let op = {
                        "+": this::this.add,
                        "-": this::this.subtract,
                        "*": this::this.multiply
                    }[stack.pop()];
                    stack.push(op(p1, p2));
                    if (arguments.length) {
                        do_operation(s);
                    } else {
                        do_operation();
                    }
                } else {
                    stack.push(s, stack.pop());
                }
            }
        }.bind(this);
        var parse_symbol = function() {
            if (string.slice(offset, offset + symbol.length) === symbol) {
                stack.push(new PolynomialZ(0, 1));
                offset += symbol.length - 1;
                trim();
                status = 2;
            } else {
                throw_error();
            }
        };

        var stack = [];
        var status = 0;
        var length = string.length;
        var offset = 0;
        if (string[offset] === "+") {
            trim();
        } else if (string[offset] === "-") {
            stack.push("-", PolynomialZ.zero);
            trim();
        }
        while (true) {
            if (status === 0) {
                if ("0123456789".includes(string[offset])) {
                    let offset2 = offset;
                    while ("0123456789".includes(string[++offset2]));
                    stack.push(new PolynomialZ(new Integer(string.slice(offset, offset2))));
                    offset = offset2 - 1;
                    trim();
                    status = 1;
                } else {
                    parse_symbol();
                }
            } else if (status === 1) {
                if (offset === length) {
                    break;
                } else if (string[offset] === "+" || string[offset] === "-") {
                    do_operation(string[offset]);
                    trim();
                    status = 3;
                } else if (string[offset] === "*") {
                    stack.push("*", stack.pop());
                    trim();
                    parse_symbol();
                } else {
                    throw_error();
                }
            } else if (status === 2) {
                if (string[offset] === "^") {
                    trim();
                    if ("0123456789".includes(string[offset])) {
                        let offset2 = offset;
                        while ("0123456789".includes(string[++offset2]));
                        let object = {};
                        object[string.slice(offset, offset2)] = 1;
                        stack[stack.length - 1] = new PolynomialZ().set(object);
                        offset = offset2 - 1;
                        trim();
                        status = 4;
                    } else {
                        throw_error();
                    }
                } else {
                    status = 4;
                }
            } else if (status == 3) {
                if ("0123456789".includes(string[offset])) {
                    let offset2 = offset;
                    while ("0123456789".includes(string[++offset2]));
                    stack.push(new PolynomialZ(new Integer(string.slice(offset, offset2))));
                    offset = offset2 - 1;
                    trim();
                    status = 1;
                } else {
                    parse_symbol();
                }
            } else if (status === 4) {
                if (offset === length) {
                    break;
                } else if (string[offset] === "+" || string[offset] === "-") {
                    do_operation(string[offset]);
                    trim();
                    status = 3;
                } else {
                    throw_error();
                }
            }
        }

        do_operation();
        this._coeff = stack[0]._coeff;
    }

    toString(symbol = "x") {
        var data = this._coeff;
        if (data.length === 0) {
            return "0";
        }
        var list = [];
        if (data[0].sign) {
            list.push(`${data[0]}`);
        }
        for (let degree = 1, length = data.length; degree < length; ++degree) {
            let coeff = data[degree];
            if (coeff.sign) {
                if (eq(abs(coeff), one)) {
                    if (list.length === 0) {
                        if (coeff.sign < 0) {
                            list.push("-");
                        }
                    } else {
                        list.push(coeff.sign > 0 ? " + " : " - ");
                    }
                } else {
                    if (list.length === 0) {
                        list.push(`${coeff}*`);
                    } else {
                        list.push(coeff.sign > 0 ? " + " : " - ", `${abs(coeff)}*`);
                    }
                }
                list.push(degree === 1 ? symbol : `${symbol}^${degree}`);
            }
        }
        return list.join("");
    }

    square(p) {
        var data = PolynomialZ.instance.from(p)._coeff;
        var length = data.length;
        if (length === 0) {
            return PolynomialZ.zero;
        }
        var result = [square(data[0])];
        for (let i = 1; i < length; ++i) {
            result.push(zero, square(data[i]));
            for (let j = 0; j < i; ++j) {
                result[i + j] = add(result[i + j], shl(multiply(data[i], data[j]), 1));
            }
        }
        return this._create(result);
    }

    divmod(p1, p2) {
        var instance = PolynomialZ.instance;
        p1 = instance.from(p1);
        p2 = instance.from(p2);
        var data1 = p1._coeff;
        var data2 = p2._coeff;
        var l2 = data2.length;
        if (l2 === 0) {
            p1 = `${p1}`;
            if (p1.includes(" ")) {
                p1 = `(${p1})`;
            }
            throw new ZeroDivisionError(`${p1} / 0`);
        } else if (data1.length === 0 || (l2 === 1 && eq(data2[0], one))) {
            return [p1, PolynomialZ.zero];
        } else if (data1.length < l2) {
            return [PolynomialZ.zero, p1];
        }
        data1 = data1.slice();
        var offset = data1.length - l2;
        var leading_coeff = data2[l2 - 1];
        var quotient = new Array(offset + 1).fill(zero);
        while (offset >= 0) {
            let [q, r] = divmod(data1[data1.length - 1], leading_coeff);
            if (r.sign) {
                p1 = `${p1}`;
                if (p1.includes(" ")) {
                    p1 = `(${p1})`;
                }
                p2 = `${p2}`;
                if (p2.includes(" ")) {
                    p2 = `(${p2})`;
                }
                throw new CannotDivideError(`${p1} / ${p2}`);
            }
            quotient[offset] = q;
            for (let i = 0; i < l2; ++i) {
                data1[i + offset] = subtract(data1[i + offset], multiply(q, data2[i]));
            }
            offset = data1::trim().length - l2;
        }
        return [instance._create(quotient), instance._create(data1)];
    }

    mod(p1, p2) {
        p1 = PolynomialZ.instance.from(p1);
        p2 = PolynomialZ.instance.from(p2);
        var data1 = p1._coeff;
        var data2 = p2._coeff;
        var l2 = data2.length;
        if (l2 === 0) {
            p1 = `${p1}`;
            if (p1.includes(" ")) {
                p1 = `(${p1})`;
            }
            throw new ZeroDivisionError(`${p1} / 0`);
        } else if (data1.length === 0 || l2 === 1) {
            return PolynomialZ.zero;
        } else if (data1.length < l2) {
            return p1;
        }
        data1 = data1.slice();
        var offset = data1.length - l2;
        var leading_coeff = data2[l2 - 1];
        while (offset >= 0) {
            let [q, r] = divmod(data1[data1.length - 1], leading_coeff);
            if (r.sign) {
                p1 = `${p1}`;
                if (p1.includes(" ")) {
                    p1 = `(${p1})`;
                }
                p2 = `${p2}`;
                if (p2.includes(" ")) {
                    p2 = `(${p2})`;
                }
                throw new CannotDivideError(`${p1} / ${p2}`);
            }
            for (let i = 0; i < l2; ++i) {
                data1[i + offset] = subtract(data1[i + offset], multiply(q, data2[i]));
            }
            offset = data1::trim().length - l2;
        }
        return PolynomialZ.instance._create(data1);
    }

    primitive() {
        var coeff = this._coeff;
        if (coeff.length === 0) {
            return PolynomialZ.zero;
        }
        var g = gcd(...this);
        if (coeff[coeff.length - 1].sign < 0) {
            g = negate(g);
        }
        return PolynomialZ.instance._create(coeff.map(n => exactDivide(n, g)));
    }
};

PolynomialZ.zero = new PolynomialZ();
PolynomialZ.one = new PolynomialZ(1);
PolynomialZ.instance = new PolynomialZ();
